// fast4j - Fast & Agile Service Tools for Java
// Copyright (C) 2007 Alexandre ROMAN
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

// $Id: ServiceInvoker.java 27 2007-03-26 19:48:20Z alexandre.roman $

package fast4j.ejbbridge.ejb;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.WeakHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ListableBeanFactory;

import fast4j.ejbbridge.ServiceBridgeException;
import fast4j.ejbbridge.ServiceCall;
import fast4j.ejbbridge.ServiceInvocationFailedException;
import fast4j.ejbbridge.ServiceMethodNotFoundException;
import fast4j.ejbbridge.ServiceNotFoundException;
import fast4j.ejbbridge.util.MethodUtils;

/**
 * Service invoker. This class is used by {@link ServiceBridgeBean} to invoke
 * service methods from a Spring context. This class is made
 * {@link Serializable} to allow the EJB to be {@link Serializable}.
 * @author alexandre.roman
 */
class ServiceInvoker implements Serializable {
    private static final long serialVersionUID = 1L;
    private final transient Logger log = LoggerFactory.getLogger(getClass());
    private final transient Map methodCache = new WeakHashMap();
    
    public Object invoke(ListableBeanFactory beanFactory, ServiceCall call,
            Map context) throws ServiceBridgeException {
        Method method = null;
        final Class serviceClass;
        try {
            serviceClass = Class.forName(call.getClassName());
        } catch (ClassNotFoundException e) {
            throw new ServiceBridgeException("Failed to load service class: "
                    + call.getClassName(), e, call);
        }
        final int hash = call.getMethodHash();
        
        synchronized (methodCache) {
            method = (Method) methodCache.get(new Integer(hash));
            if (method == null) {
                final Method[] methods = serviceClass.getMethods();
                for (int i = 0; i < methods.length; ++i) {
                    final Method candidate = methods[i];
                    if (Object.class.equals(candidate.getDeclaringClass())) {
                        // filter methods declared in Object, such as toString,
                        // equals, etc...
                        continue;
                    }
                    
                    // look for required method
                    final int hashCandidate = MethodUtils.hash(candidate);
                    if (log.isDebugEnabled()) {
                        final String candidateStr = candidate.toGenericString();
                        log.debug("Caching service method: {}", candidateStr);
                    }
                    // as we are iterating, we cache all methods for
                    // faster access next time
                    methodCache.put(new Integer(hashCandidate), candidate);
                    if (hash == hashCandidate) {
                        // method found
                        method = candidate;
                    }
                    // continue, even if the method was found: this enables
                    // to cache all methods
                }
            }
        }
        
        if (method == null) {
            throw new ServiceMethodNotFoundException(
                    "Failed to find method in class '" + serviceClass.getName()
                            + "' with hash: " + hash, call);
        }
        
        final Map services = beanFactory.getBeansOfType(serviceClass);
        if (services.isEmpty()) {
            throw new ServiceNotFoundException(
                    "Failed to locate service of type: "
                            + serviceClass.getName(), call);
        }
        
        // get service (if there are more than one entry, get the first one)
        final Map.Entry firstEntry = (Map.Entry) services.entrySet().iterator()
                .next();
        if (services.size() > 1) {
            log.warn(
                "Several services were found with type '{}': using bean '{}'",
                serviceClass.getName(), firstEntry.getKey());
        }
        
        final Object service = firstEntry.getValue();
        try {
            return method.invoke(service, call.getArguments());
        } catch (IllegalAccessException e) {
            throw new ServiceInvocationFailedException(
                    "Failed to call method in class '" + serviceClass.getName()
                            + "': " + e, e, call);
        } catch (InvocationTargetException e) {
            throw new ServiceInvocationFailedException(
                    "Failed to call method in class '" + serviceClass.getName()
                            + "': " + e.getTargetException(), e
                            .getTargetException(), call);
        }
    }
}
